/**
 * Copyright (C) 2009-2012, KingHool.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses.
 */
package com.kinghool.smodel;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.kinghool.ebos.commons.utils.Assert;
import com.kinghool.ebos.commons.utils.NameHelper;
import com.kinghool.ebos.commons.utils.Utils;
import com.kinghool.smodel.def.TreeModelDefinition;
import com.kinghool.smodel.query.Query;

/**
 * 
 * @Author Allen Deng(dengtailin@gmail.com) 
 */
public class TreeModel extends Model implements ITreeModel {
	protected ITreeModel parent; 
	protected List<String> childNames = Utils.newList();
	protected Map<String, List<ITreeModel>> childrenMap = Utils.newMap();
	protected List<ITreeModel> childrenList = Utils.newList();
   
	public TreeModel(ITreeModel parent, TreeModelDefinition modelDefinition) {
		super(modelDefinition);
		this.parent = parent;
	}  
	
	public TreeModel(TreeModelDefinition modelDefinition, ITreeModel parent) {
		this(parent, modelDefinition);
	}
	
	public TreeModel(TreeModelDefinition modelDefinition) {
		this(modelDefinition, null);
	}
	
	public TreeModel(String id, ITreeModel parent) {
		this(parent, TreeModelDefinition.createDefaultTreeModelDefinition(id, parent == null ? null : (TreeModelDefinition) parent.getModelDefinition()));
	}	
	
	public TreeModel(String id) {
		this(id, null);
	}
	
	public TreeModel(ITreeModel parent) {
		this(parent, TreeModelDefinition.createDefaultTreeModelDefinition(parent.getModelId(), 
				parent == null ? null : (TreeModelDefinition) parent.getModelDefinition()));
	} 
	
	public TreeModelDefinition getModelDefinition() {
		return (TreeModelDefinition) modelDefinition;
	}  

	public ITreeModel getRoot(){
		ITreeModel root = this;
		while (root.getParent() != null) {
			root = root.getParent();
		}
		return root;
	}
	
	public ITreeModel getParent() { 
		return parent;
	}
	
	public void setParent(ITreeModel parent) {
		this.parent = parent;
	}

	public boolean isRoot() {
		return parent == null;
	}
	
	public void addChild(ITreeModel child) {
		installChild(child, true);
	} 
	
	protected void installChild(ITreeModel child, boolean...notify) {
		checkChild(child.getModelId());
		String childName = formatName(child.getModelId());

		if (!childNames.contains(childName)) {
			childNames.add(childName);
		}
		
		child.setParent(this);
		
		List<ITreeModel> subChildren = childrenMap.get(childName);
		if (subChildren == null) {
			subChildren = Utils.newList();
		}
		subChildren.add(child);
		
		childrenMap.put(childName, subChildren);
		childrenList.add(child);
		
		if (notify != null && notify.length > 0  && notify[0]) {	 
			//Fire add child event
			firePropertyChange(this, childName, null, child);
		} 
	}
	 
	public void removeChild(int index) {
        ITreeModel child = childrenList.get(index);
        if (child != null) {
        	removeTreeChild(child.getModelId(), child);
        }
	} 
	
	public void removeChild(ITreeModel child) {
		removeTreeChild(child.getModelId(), child);
	} 
	
	public void removeTreeChild(String childFullName, Query... queries) {
		childFullName = formatName(childFullName);
		
		List<ITreeModel> childParents = getParentsOfChild(Arrays.asList(new ITreeModel[]{this}), childFullName);
		for (ITreeModel childParent : childParents) {
			Object removed = ((TreeModel)childParent).removeDirectTreeChild(NameHelper.getChildName(childFullName), queries);
			firePropertyChange(this, NameHelper.getChildName(childFullName), removed, null);
		}
	} 
 
	public void removeTreeChild(String childFullName, int index) {
		childFullName = formatName(childFullName);
		List<ITreeModel> childParents = getParentsOfChild(Arrays.asList(new ITreeModel[]{this}), childFullName);
		for (ITreeModel childParent : childParents) {
			Object removed = ((TreeModel)childParent).removeDirectTreeChild(NameHelper.getChildName(childFullName), index);
			firePropertyChange(this, NameHelper.getChildName(childFullName), removed, null);
		}
	}

	public void removeTreeChild(String childFullName, ITreeModel child) {
		childFullName = formatName(childFullName);
		List<ITreeModel> childParents = getParentsOfChild(Arrays.asList(new ITreeModel[]{this}), childFullName);
		for (ITreeModel childParent : childParents) {
			Object removed = ((TreeModel)childParent).removeDirectTreeChild(NameHelper.getChildName(childFullName), Arrays.asList(new ITreeModel[]{child}));
			firePropertyChange(this, NameHelper.getChildName(childFullName), removed, null);
		}
	}   
	
	protected Object removeDirectTreeChild(String childName, Query... queries) {
		checkChild(childName);	 
	    childName = formatName(childName);
		if (childNames.contains(childName)) {
			List<ITreeModel> mapChildrenLst = childrenMap.get(childName);
			List<ITreeModel> rmvChildrenLst = Utils.newList();
			for (ITreeModel child : mapChildrenLst) {
				if (matchModel(child, queries)) {
					rmvChildrenLst.add(child);
				}
			}
			return removeDirectTreeChild(childName, rmvChildrenLst);
		}
		return null;
	}
	
	protected Object removeDirectTreeChild(String childName, List<ITreeModel> rmvChildrenLst) {
		checkChild(childName);	 
	    childName = formatName(childName);
		if (childNames.contains(childName)) {
			List<ITreeModel> mapChildrenLst = childrenMap.get(childName);
			if (mapChildrenLst.containsAll(rmvChildrenLst) && childrenList.containsAll(rmvChildrenLst)) {
				childrenList.removeAll(rmvChildrenLst);
				mapChildrenLst.removeAll(rmvChildrenLst);
				if (mapChildrenLst.size() == 0) {
					childrenMap.remove(childName);
					childNames.remove(childName);
				}
				return rmvChildrenLst;
			}
		}
		return null;
	}
	
	protected Object removeDirectTreeChild(String childName, int index) {
		checkChild(childName);	 
	    childName = formatName(childName);
		if (childNames.contains(childName)) {
			List<ITreeModel> mapChildrenLst = childrenMap.get(childName);
			if (mapChildrenLst.size() > index && index >= 0) {
				ITreeModel rmvChild = mapChildrenLst.remove(index);
				childrenList.remove(rmvChild);
				if (mapChildrenLst.size() == 0) {
					childrenMap.remove(childName);
					childNames.remove(childName);
				}
				return rmvChild;
			}
		}
		return null;
	}
	
	public List<String> getChildNames() { 
		return Collections.unmodifiableList(childNames);
	}  
	
	protected void checkChild(String childFullName) { 
		if (!isTreeChild(childFullName)) {
			throw new RuntimeException("The model has no child " + childFullName + " defined!");
		}
	}	
	
	public void reset(){
		super.reset();
		childNames.clear();
		childrenMap.clear();
		childrenList.clear();
	} 
	  
	public ITreeModel getFirstTreeChild(String childFullName, Query... queries) {
		childFullName = formatName(childFullName);
		return getTreeChild(childFullName, 0, queries);
	}

	public List<ITreeModel> getTreeChild(String childFullName, Query... queries) {
		childFullName = formatName(childFullName);
		List<ITreeModel> children = Utils.newList();
		List<ITreeModel> childParents = getParentsOfChild(Arrays.asList(new ITreeModel[]{this}), childFullName);
        for (ITreeModel childParent : childParents) {
        	children.addAll(((TreeModel)childParent).getDirectChild(NameHelper.getChildName(childFullName), queries));
        }
		return children;
	} 

	public List<ITreeModel> getChild(Query... queries) {
		List<ITreeModel> foundChildren = Utils.newList(); 
		for (ITreeModel child : childrenList) {
			if (matchModel(child, queries)) {
				foundChildren.add(child);
			}
		} 
		return foundChildren;
	}
	
	public ITreeModel getChild(int index, Query... queries) {
		List<ITreeModel> children = getChild(queries);
		return children.size() > index && index >= 0 ? children.get(index) : null;
	}

	public ITreeModel getTreeChild(String childFullName, int index, Query... queries) {
		childFullName = formatName(childFullName);
		List<ITreeModel> children = getTreeChild(childFullName, queries);
		return children.size() > index && index >= 0 ? children.get(index) : null;
	}

	public ITreeModel queryFirstTreeChild(String queryStr) {
		return TreeModelLocator.locateFirstTreeModel(this, queryStr);
	}
	
	public List<ITreeModel> queryTreeChild(String queryStr) {
		List<ITreeModel> srcTreeModels = Utils.newList();
		srcTreeModels.addAll(Arrays.asList(new ITreeModel[]{this}));
		return TreeModelLocator.locateTreeModels(srcTreeModels, queryStr);
	}
	
	public boolean hasTreeChild(String childFullName, Query... queries) {
		childFullName = formatName(childFullName);
		return !Assert.isNullOrEmpty(getTreeChild(childFullName, queries));
	}

	public boolean isTreeChild(String childFullName) {
		childFullName = formatName(childFullName);
		return getModelDefinition().isTreeChild(childFullName);
	} 
	
	public void moveChild(int sourceIndex, int targetIndex) {
		if (sourceIndex == targetIndex) return;
		ITreeModel source = childrenList.get(sourceIndex);
		String childName = source.getModelId();

		if (Assert.isNotNullNotEmpty(childName)) {
			moveDirectChild(childName, source, getTargetModelInMap(childName, targetIndex));
			moveDirectChild(sourceIndex, targetIndex);
			firePropertyChange(this, childName, sourceIndex, targetIndex);
		}
	}
	 
	public void moveChild(ITreeModel source, ITreeModel target) {
		if (childrenList.contains(source) && childrenList.contains(target)) {
			int sourceIndex = childrenList.indexOf(source);
			int targetIndex = childrenList.indexOf(target);
		    moveChild(sourceIndex, targetIndex);
		}
	}
	
	public void moveTreeChild(String childFullName, int sourceIndex, int targetIndex) {
		childFullName = formatName(childFullName);
		List<ITreeModel> childParents = getParentsOfChild(Arrays.asList(new ITreeModel[]{this}), childFullName);
		for (ITreeModel childParent : childParents) {
			ITreeModel source = ((TreeModel)childParent).getDirectChild(NameHelper.getChildName(childFullName), sourceIndex);
			ITreeModel target = ((TreeModel)childParent).getDirectChild(NameHelper.getChildName(childFullName), targetIndex);
            if (source != null && target != null) {
            	moveTreeChild(childFullName, source, target);
            }
		}
	}

	public void moveTreeChild(String childFullName, ITreeModel source, ITreeModel target) {
		childFullName = formatName(childFullName);
		List<ITreeModel> childParents = getParentsOfChild(Arrays.asList(new ITreeModel[]{this}), childFullName);
		for (ITreeModel childParent : childParents) {
			((TreeModel)childParent).moveDirectChild(NameHelper.getChildName(childFullName), 
															source,
															target);
			((TreeModel)childParent).moveDirectChild(((TreeModel)childParent).getChildIndex(source), ((TreeModel)childParent).getChildIndex(target));
			firePropertyChange(this, NameHelper.getChildName(childFullName), source, target);
		}
	}
	
	protected int getChildIndex(ITreeModel child) {
		return childrenList.indexOf(child);
	}
	  
	protected String moveDirectChild(int sourceIndex, int targetIndex) {
		if (sourceIndex < childrenList.size() 
			&& sourceIndex >= 0 
			&& targetIndex < childrenList.size() 
			&& targetIndex >= 0 
			&& sourceIndex != targetIndex
			) {
			ITreeModel source = childrenList.remove(sourceIndex);
			childrenList.add(targetIndex, source);
			return source.getModelId();
		} 
		return null;
	}
	
	private ITreeModel getTargetModelInMap(String childName, int lstIndex) {
		checkChild(childName);	 
	    childName = formatName(childName);
	    List<ITreeModel> childList = childrenMap.get(childName);

	    for (ITreeModel child : childList) {
		   if (childrenList.indexOf(child) >= lstIndex) {
			   return child;
		   }
	    }
	    return null;
	}
	
	protected void moveDirectChild(String childName, int sourceIndex, int targetIndex) {
		checkChild(childName);	 
	    childName = formatName(childName);
		if (childrenMap.containsKey(childName)) {
			List<ITreeModel> childList = childrenMap.get(childName);
			if (sourceIndex < childList.size() 
				&& sourceIndex >= 0 
				&& targetIndex < childList.size() 
				&& targetIndex >= 0 
				&& sourceIndex != targetIndex
				)  {
				ITreeModel source = childList.remove(sourceIndex);
				childList.add(targetIndex, source);
			} 
		}
	}
	
	protected void moveDirectChild(String childName, ITreeModel source, ITreeModel target) {
		checkChild(childName);	 
	    childName = formatName(childName);
		if (childrenMap.containsKey(childName)) {
			List<ITreeModel> childList = childrenMap.get(childName);
			if (childList.contains(source) && childList.contains(target)) {
				int sourceIndex = childList.indexOf(source);
				int targetIndex = childList.indexOf(target);
				moveDirectChild(childName, sourceIndex, targetIndex);
			}
		}
	}
	
	protected List<ITreeModel> getParentsOfChild(List<ITreeModel> models, String childFullName) {
		List<ITreeModel> parents = Utils.newList();
		String[] childNames = NameHelper.parseFullName(childFullName);
		
		if (childNames.length == 1) {
			return models;
		}
		
		for (int i = 0; i < childNames.length - 1; i++) {
			for (ITreeModel model : models) {
				List<ITreeModel> subChildren = ((TreeModel)model).getDirectChild(childNames[i]);
				parents.addAll(getParentsOfChild(subChildren, NameHelper.removeFirstNames(childFullName, 1)));
			}
		}
		return parents;
	}
	
	protected ITreeModel getDirectChild(String childName, int index) {
		checkChild(childName);	 
	    childName = formatName(childName);
		if (childrenMap.containsKey(childName)) {
			List<ITreeModel> children = childrenMap.get(childName);
			if (index >= 0 && index < children.size()) {
				return children.get(index);
			}
		}
		
		return null;
	}
	
	protected List<ITreeModel> getDirectChild(String childName, Query... queries) {
		checkChild(childName);	 
	    childName = formatName(childName);
		if (childrenMap.get(childName) == null) {
			childrenMap.put(childName, new ArrayList<ITreeModel>());
		}  
		
		List<ITreeModel> subChildren = Utils.newList();
		for (ITreeModel child : childrenMap.get(childName)) { 
			if (matchModel(child, queries)) {
				subChildren.add(child);
			}
		}
		return subChildren;
	}
	
	public void firePropertyChange(Object notifier, String property, Object oldValue, Object newValue) {
		super.firePropertyChange(this, property, oldValue, newValue);
		if (this.getParent() != null) {
			this.getParent().firePropertyChange(this, property, oldValue, newValue);
		}		 
	}
	
	public String toString() {
		if (getParent() instanceof ITreeModel) {
			ITreeModel parent = (ITreeModel) getParent();
			return NameHelper.buildFullName(parent.toString(), getModelId());
		} else {
			return getModelId();
		}
	}

	public Map<String, List<ITreeModel>> getAllChildren() {
		return Collections.unmodifiableMap(childrenMap);
	}

	public ITreeModel addChild(String childFullName, Map<String, ? extends Object> propValPairs) {
		ITreeModel child = createChild(childFullName, getModelDefinition(), this, true);
		 
		if (child != null && !Assert.isNullOrEmpty(propValPairs)) {
			for (String attr : propValPairs.keySet()) {
				child.setProperty(attr, propValPairs.get(attr));
			}
		}
		return child;
	}
	
	public ITreeModel addChild(String childFullName) {
		return addChild(childFullName, new HashMap<String, Object>());
	}
	 
	public void clarify() {
		new ElementResourceDelta((ITreeModel) getRoot()).accept(new ElementResourceVistor(){
			public boolean visit(ITreeModel model)  {
				return !((TreeModel)model).isBlank();
			}
		});
	}
	
	protected ITreeModel createChild(String childFullName, TreeModelDefinition treeModelDefinition, ITreeModel parent, boolean force, Query... queries) {
		ITreeModel createdChild = null;
		if (! Assert.isNullOrEmpty(childFullName)) {
			String[] childNames = NameHelper.parseFullName(childFullName);
			if (childNames.length > 0) { 
				for (int i = 0; i < childNames.length - 1; i++) {				
					ITreeModel child = parent.getFirstTreeChild(childNames[i]);
					if (child == null) {
						child = createChild(parent, childNames[i], treeModelDefinition);	 
					} 
					 
					parent = child;
					treeModelDefinition = getNotNullChildModelDefinition(childNames[i], treeModelDefinition); 
				}
				createdChild = parent.getFirstTreeChild(childNames[childNames.length - 1], queries);
				if (force || createdChild == null) {
					createdChild = createChild(parent, childNames[childNames.length - 1], treeModelDefinition);	 
				}
			}
		}  
		return createdChild;
	} 
	
	protected TreeModelDefinition getNotNullChildModelDefinition(String childId, TreeModelDefinition modelDefinition) {
		return TreeModelDefinition.getNotNullChildTreeModelDefinition(childId, modelDefinition);
	}
	
	protected ITreeModel createChild(ITreeModel parent, String childName, TreeModelDefinition modelDefinition) {
		TreeModelDefinition childModelDef = getNotNullChildModelDefinition(childName, modelDefinition); 
		ITreeModel createdChild = new TreeModel(parent, childModelDef);	 
		parent.addChild(createdChild); 
		
		return createdChild;
	}   
	
	protected boolean isBlank() {
		if (Assert.isNullOrEmpty(getAllProperties()) 
			&& !hasChildren() 
			&& (getModelDefinition().isIgnoreCheck() || !getModelDefinition().isMandantory())) {
			return true;
		} 
		return false;
	}
	
	public boolean hasChildren()  { 
		if (Assert.isNullOrEmpty(getAllChildren())) {
			return false;
		} 
		for (String childName : getChildNames()) {
			if (! Assert.isNullOrEmpty(getTreeChild(childName))) {
				return true;
			}
		}
		return false;
	}
	
	protected boolean matchModel(IModel model, Query... criterias) {
		boolean valid = true;
		if (!Assert.isNullOrEmpty(criterias)) {
			for (Query criteria : criterias) {
				if (criteria == null) continue;
				if (!criteria.isMatched(model)) {
				    valid = false;
				    break;
				} 
			} 
		} 
		return valid;
	}
	
	@SuppressWarnings("unchecked")
	public ITreeModel clone() throws CloneNotSupportedException {
		TreeModel clone = (TreeModel) super.clone();
		clone.childNames = (List<String>) ((ArrayList<String>)childNames).clone();
	 
		clone.childrenList = Utils.newList();
		clone.childrenMap = Utils.newMap();
		for (ITreeModel child : childrenList) {
			TreeModel cloneChild = (TreeModel) child.clone();
			
			//do not clone parent, set parent from its parents clone method to keep relation.
			cloneChild.parent = clone;
			if (cloneChild.modelDefinition instanceof TreeModelDefinition
				&& clone.modelDefinition instanceof TreeModelDefinition) {
				((TreeModelDefinition)cloneChild.modelDefinition).setParent((TreeModelDefinition) clone.modelDefinition);
			}
			clone.childrenList.add(cloneChild);

			String childName = cloneChild.getModelId();
			if (!clone.childrenMap.containsKey(childName)) {
				clone.childrenMap.put(childName, new ArrayList<ITreeModel>());
			}
			clone.childrenMap.get(childName).add(cloneChild);
		}
		
		return clone;
	}
	
	class ElementResourceDelta{
		private ITreeModel elementResource;
		public ElementResourceDelta(ITreeModel elementResource) {
			this.elementResource = elementResource;
		}
		
		public void accept(final ElementResourceVistor visitor){
			for (String childName : new ArrayList<String>(elementResource.getChildNames())) {
				for (ITreeModel child : new ArrayList<ITreeModel>(elementResource.getTreeChild(childName))) {
					if (child instanceof ITreeModel) {
						new ElementResourceDelta((ITreeModel) child).accept(visitor);
					}
				}
			}
			if (! visitor.visit(elementResource)) {
				if (elementResource.getParent() != null) {
					 elementResource.getParent().removeChild(elementResource);
				}
			}
		}
	}
	
	interface ElementResourceVistor {
		public boolean visit(ITreeModel model);
	}
}
